/**
 * 
 */
package org.swing.utility.reflection.util;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * @author lqnhu
 *
 */
public class ReflectionUtil
{
	
	private Class<?> targetClass;
    private Map<String, LinkedList<Method>> methodMap = null;       // used for the brute-force method finder

    public ReflectionUtil( Class<?> targetClass ) {
        if ( targetClass == null ) {
            throw new IllegalArgumentException("ReflectionHelper.errorConstructing"); //$NON-NLS-1$
        }
        this.targetClass = targetClass;
    }

    public Method findBestMethodOnTarget( String methodName, Object[] arguments ) throws NoSuchMethodException, SecurityException {
    	if (arguments == null) {
    		return findBestMethodWithSignature(methodName, Collections.EMPTY_LIST);
    	}
        int size = arguments.length;
        List<Class<?>> argumentClasses = new ArrayList<Class<?>>(size);
        for (int i=0; i!=size; ++i) {
            if ( arguments[i] != null ) {
                Class<?> clazz = arguments[i].getClass();
                argumentClasses.add( clazz );
            } else {
                argumentClasses.add(null);
            }
        }
        return findBestMethodWithSignature(methodName,argumentClasses);
    }

    public Method findBestMethodWithSignature( String methodName, Object[] argumentsClasses ) throws NoSuchMethodException, SecurityException {
        List argumentClassesList = Arrays.asList(argumentsClasses);
        return findBestMethodWithSignature(methodName,argumentClassesList);
    }

    public Method findBestMethodWithSignature( String methodName, List<Class<?>> argumentsClasses ) throws NoSuchMethodException, SecurityException {
        // Attempt to find the method
        Method result = null;
        Class[] classArgs = new Class[argumentsClasses.size()];

        // -------------------------------------------------------------------------------
        // First try to find the method with EXACTLY the argument classes as specified ...
        // -------------------------------------------------------------------------------
        try {
            argumentsClasses.toArray(classArgs);
            result = this.targetClass.getMethod(methodName,classArgs);  // this may throw an exception if not found
            return result;
        } catch ( NoSuchMethodException e ) {
            // No method found, so continue ...
        }

        // ---------------------------------------------------------------------------------------------
        // Then try to find a method with the argument classes converted to a primitive, if possible ...
        // ---------------------------------------------------------------------------------------------
        List<Class<?>> argumentsClassList = convertArgumentClassesToPrimitives(argumentsClasses);
        argumentsClassList.toArray(classArgs);
        try {
            result = this.targetClass.getMethod(methodName,classArgs);  // this may throw an exception if not found
            return result;
        } catch ( NoSuchMethodException e ) {
            // No method found, so continue ...
        }

        // ---------------------------------------------------------------------------------------------
        // Still haven't found anything.  So far, the "getMethod" logic only finds methods that EXACTLY
        // match the argument classes (i.e., not methods declared with superclasses or interfaces of
        // the arguments).  There is no canned algorithm in Java to do this, so we have to brute-force it.
        // ---------------------------------------------------------------------------------------------
        if ( this.methodMap == null ) {
            this.methodMap = new HashMap<String, LinkedList<Method>>();
            Method[] methods = this.targetClass.getMethods();
            for ( int i=0; i!=methods.length; ++i ) {
                Method method = methods[i];
                LinkedList<Method> methodsWithSameName = this.methodMap.get(method.getName());
                if ( methodsWithSameName == null ) {
                    methodsWithSameName = new LinkedList<Method>();
                    this.methodMap.put(method.getName(),methodsWithSameName);
                }
                methodsWithSameName.addFirst(method);   // add lower methods first
            }
        }

        LinkedList<Method> methodsWithSameName = this.methodMap.get(methodName);
        if ( methodsWithSameName == null ) {
            throw new NoSuchMethodException(methodName);
        }
        for (Method method : methodsWithSameName) {
            Class[] args = method.getParameterTypes();
			boolean allMatch = argsMatch(argumentsClasses, argumentsClassList, args);
            if ( allMatch ) {
                if (result != null) {
                    throw new NoSuchMethodException(methodName + " Args: " + argumentsClasses + " has multiple possible signatures."); //$NON-NLS-1$ //$NON-NLS-2$
                }
                result = method;
            }
        }
        
        if (result != null) {
            return result;
        }

        throw new NoSuchMethodException(methodName + " Args: " + argumentsClasses); //$NON-NLS-1$
    }

	private static boolean argsMatch(List<Class<?>> argumentsClasses,
			List<Class<?>> argumentsClassList, Class[] args) {
        if ( args.length != argumentsClasses.size() ) {
            return false;
        }
		for ( int i=0; i<args.length; ++i ) {
		    Class<?> primitiveClazz = argumentsClassList.get(i);
		    Class<?> objectClazz = argumentsClasses.get(i);
		    if ( objectClazz != null ) {
		        // Check for possible matches with (converted) primitive types
		        // as well as the original Object type 
		        if ( ! args[i].equals(primitiveClazz) && ! args[i].isAssignableFrom(objectClazz) ) {
		            return false;   // found one that doesn't match
		        }
		    } else {
		        // a null is assignable for everything except a primitive
		        if ( args[i].isPrimitive() ) {
		            return false;   // found one that doesn't match
		        }
		    }
		}
		return true;
	}
    
    /**
     * Convert any argument classes to primitives.
     * @param arguments the list of argument classes.
     * @return the list of Class instances in which any classes that could be represented
     * by primitives (e.g., Boolean) were replaced with the primitive classes (e.g., Boolean.TYPE).
     */
    private static List<Class<?>> convertArgumentClassesToPrimitives( List<Class<?>> arguments ) {
        List<Class<?>> result = new ArrayList<Class<?>>(arguments.size());
        for (Class<?> clazz : arguments) {
            if      ( clazz == Boolean.class   ) clazz = Boolean.TYPE;
            else if ( clazz == Character.class ) clazz = Character.TYPE;
            else if ( clazz == Byte.class      ) clazz = Byte.TYPE;
            else if ( clazz == Short.class     ) clazz = Short.TYPE;
            else if ( clazz == Integer.class   ) clazz = Integer.TYPE;
            else if ( clazz == Long.class      ) clazz = Long.TYPE;
            else if ( clazz == Float.class     ) clazz = Float.TYPE;
            else if ( clazz == Double.class    ) clazz = Double.TYPE;
            else if ( clazz == Void.class      ) clazz = Void.TYPE;
            result.add( clazz );
        }

        return result;
    }
    
    /**
     * Helper method to load a class.
     * @param className is the class to instantiate
     * @param classLoader the class loader to use; may be null if the current
     * class loader is to be used
     * @return Class is the instance of the class 
     * @throws ClassNotFoundException 
     */
    private static final Class<?> loadClass(final String className, final ClassLoader classLoader) throws ClassNotFoundException {
        Class<?> cls = null;
        if ( classLoader == null ) {
            cls = Class.forName(className.trim());
        } else {
            cls = Class.forName(className.trim(),true,classLoader);
        }
        return cls;
    }
    
    /**
     * Helper method to create an instance of the class using the appropriate
     * constructor based on the ctorObjs passed.
     * @param className is the class to instantiate
     * @param ctorObjs are the objects to pass to the constructor; optional, nullable
     * @param classLoader the class loader to use; may be null if the current
     * class loader is to be used
     * @return Object is the instance of the class 
     * @throws Exception if an error occurs instantiating the class
     */

    public static final Object create(String className, Collection<?> ctorObjs, 
                                      final ClassLoader classLoader) throws Exception {
    	try {
	        int size = (ctorObjs == null ? 0 : ctorObjs.size());
	        Class[] names = new Class[size];
	        Object[] objArray = new Object[size];
	        int i = 0;
	
	        if (size > 0) {
	            for (Iterator<?> it=ctorObjs.iterator(); it.hasNext(); ) {
	                Object obj = it.next();
	                if (obj != null) {
		                names[i] = obj.getClass();
		                objArray[i] = obj;
	                }
	                i++;
	            }
	        } 
	        return create(className, objArray, names, classLoader);
    	} catch (Exception e) {
    		throw new Exception(e);
    	}
    }
    	
    public static final Object create(String className, Object[] ctorObjs, Class<?>[] argTypes, 
                final ClassLoader classLoader) throws Exception {
    	Class<?> cls;
        try {
            cls = loadClass(className,classLoader);
        } catch(Exception e) {
            throw new Exception(e);
        }
        Constructor<?> ctor = null;
        try {
        	ctor = cls.getDeclaredConstructor(argTypes);
        } catch (NoSuchMethodException e) {
        	
        }
        
        if (ctor == null && argTypes != null && argTypes.length > 0) {
        	List<Class<?>> argumentsClasses = Arrays.asList(argTypes);
        	List<Class<?>> argumentsClassList = convertArgumentClassesToPrimitives(argumentsClasses);
        	for (Constructor<?> possible : cls.getDeclaredConstructors()) {
        		if (argsMatch(argumentsClasses, argumentsClassList, possible.getParameterTypes())) {
        			ctor = possible;
        			break;
        		}
        	}
        }
        
        if (ctor == null) {
        	throw new Exception(className + " Args: " + Arrays.toString(argTypes)); //$NON-NLS-1$
        }
        
        try {
			return ctor.newInstance(ctorObjs);
		} catch (Exception e) {
			throw new Exception(e);
		}
    }
	
	public static Object constructObject(String className, Object[] args)
			throws IllegalArgumentException, SecurityException,
			InstantiationException, IllegalAccessException,
			InvocationTargetException, NoSuchMethodException,
			ClassNotFoundException {
		Class<?> targetClass = Thread.currentThread().getContextClassLoader()
				.loadClass(className);
		return getMatchingConstructor(targetClass, args).newInstance(args);
	}

	private static Constructor<?> getMatchingConstructor(Class<?> targetClass,
			Object[] args) throws NoSuchMethodException {
		Constructor<?>[] ctors = targetClass.getConstructors();
		for (int i = 0; i < ctors.length; i++) {
			if (isMatchingArgs(ctors[i].getParameterTypes(), args)) {
				return ctors[i];
			}
		}
		throw new NoSuchMethodException("No constructor for " + targetClass
				+ " compatible with args " + Arrays.toString(args));
	}

	public static Object callMethod(Object target, String method, Object[] args)
			throws IllegalArgumentException, SecurityException,
			IllegalAccessException, InvocationTargetException,
			NoSuchMethodException {
		return getMatchingMethod(target.getClass(), method, args).invoke(
				target, args);
	}

	public static Object callMethod(Object target, Class<?> interfaceClass,
			String method, Object[] args) throws IllegalArgumentException,
			SecurityException, IllegalAccessException,
			InvocationTargetException, NoSuchMethodException {
		return getMatchingMethod(interfaceClass, method, args).invoke(target,
				args);
	}

	private static Method getMatchingMethod(Class<?> targetClass,
			String methodName, Object[] args) throws NoSuchMethodException {
		Method[] methods = targetClass.getMethods();
		for (int i = 0; i < methods.length; i++) {
			if (methodName.equals(methods[i].getName())
					&& isMatchingArgs(methods[i].getParameterTypes(), args)) {
				return methods[i];
			}
		}
		throw new NoSuchMethodException(methodName);
	}

	private static boolean isMatchingArgs(Class<?>[] targetArgTypes,
			Object[] args) {
		if (args == null && targetArgTypes.length == 0) {
			return true;
		}
		if (args.length != targetArgTypes.length) {
			return false;
		}
		for (int j = 0; j < targetArgTypes.length; j++) {
			if (j >= args.length
					|| !isAssignable(targetArgTypes[j], args[j].getClass())) {
				return false;
			}
		}
		return true;
	}

	private static boolean isAssignable(Class<?> assignee, Class<?> assigned) {
		if (assignee.isPrimitive()) {
			if (assignee == int.class || assignee == short.class
					|| assignee == long.class || assignee == byte.class) {
				assignee = Number.class;
			} else if (assignee == double.class || assignee == float.class) {
				assignee = Double.class;
			} else if (assignee == char.class) {
				assignee = Character.class;
			} else if (assignee == boolean.class) {
				assignee = Boolean.class;
			}
		}
		return assignee.isAssignableFrom(assigned);
	}

	public static Object callStaticMethod(String methodFqn, Object[] args)
			throws IllegalArgumentException, SecurityException,
			IllegalAccessException, InvocationTargetException,
			NoSuchMethodException, ClassNotFoundException {
		int n = methodFqn.lastIndexOf('.');
		String className = methodFqn.substring(0, n);
		String methodName = methodFqn.substring(n + 1, methodFqn.length());
		Class<?> targetClass = Thread.currentThread().getContextClassLoader()
				.loadClass(className);
		return getMatchingMethod(targetClass, methodName, args).invoke(null,
				args);
	}

	public static void dumpStackTraces() {
		try {
			Object threadMXBean = ReflectionUtil.callStaticMethod(
					"java.lang.management.ManagementFactory.getThreadMXBean",
					null);
			Class<?> threadMXBeanInterface = Class
					.forName("java.lang.management.ThreadMXBean");
			long[] threadIds = (long[]) ReflectionUtil.callMethod(threadMXBean,
					threadMXBeanInterface, "getAllThreadIds", null);
			Object[] threadInfos = (Object[]) ReflectionUtil.callMethod(
					threadMXBean, threadMXBeanInterface, "getThreadInfo",
					new Object[] { threadIds, Integer.valueOf(10) });
			for (int i = 0; i < threadInfos.length; i++) {
				System.out.println((String) ReflectionUtil.callMethod(
						threadInfos[i], "getThreadName", null));
				BeanInfo info = Introspector.getBeanInfo(threadInfos[i]
						.getClass());
				PropertyDescriptor[] parameters = info.getPropertyDescriptors();
				for (int p = 0; p < parameters.length; p++) {
					if (parameters[p].getReadMethod() != null) {
						Object value = parameters[p].getReadMethod().invoke(
								threadInfos[i], (Object[]) null);
						if (value != null && value.getClass().isArray()) {
							System.out.println(" " + parameters[p].getName()
									+ ":");
							for (int a = 0; a < Array.getLength(value); a++) {
								System.out.println(" " + Array.get(value, a));
							}
						} else {
							if (value != null) {
								System.out.println(" "
										+ parameters[p].getName() + ": "
										+ value);
							}
						}
					}
				}
				System.out.println();
			}
		} catch (Exception e) {
			e.printStackTrace();
			// ignore, proabably wrong JVM version
		}
	}

	public static <T> T newInstance(Class<T> clazz) {
		try {
			return clazz.newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static Object invokeStaticMethod(String className,
			String methodName, Object... args) throws Exception {
		Class classArgs[] = new Class[args.length];
		for (int i = 0; i < args.length; i++) {
			classArgs[i] = args[i].getClass();
		}
		Class c = Class.forName(className);
		Method m = c.getDeclaredMethod(methodName, classArgs);
		Object o = null;
		try {
			o = m.invoke(null, args);
		} catch (InvocationTargetException e) {
			throw e;
		}
		return o;
	}

	public static Object invokeMethod(Object obj, String methodName,
			boolean useInterfaceParams, Object... args) throws Exception {
		try {
			Object returnObj = null;

			// sort out params
			Class[] params = new Class[args.length];
			for (int i = 0; i < args.length; i++) {
				if (useInterfaceParams) {
					// hack to use interface names with params
					for (int j = 0; j < args[i].getClass().getInterfaces().length; j++) {
						params[i] = args[i].getClass().getInterfaces()[i];
					}
				} else {
					params[i] = args[i].getClass();
				}
			}

			// retrieve method
			Method m = obj.getClass().getMethod(methodName, params);

			// execute method
			if (!m.isAccessible()) {
				m.setAccessible(true);
				returnObj = m.invoke(obj, args);
				m.setAccessible(false);
			} else {
				returnObj = m.invoke(obj, args);
			}

			return returnObj;
		} catch (NoSuchMethodException e) {
			throw e;
		} catch (IllegalAccessException e) {
			throw e;
		} catch (InvocationTargetException e) {
			throw e;
		}
	}

	public static <T> Class<T> getTemplateType(Class<?> clazz, int num) {
		return (Class<T>) getSuperTemplateTypes(clazz)[num];
	}

	public static <T> Class<T> getTemplateType(Class<?> clazz,
			Class<?> classWithParameter, int num) {
		return (Class<T>) getSuperTemplateTypes(clazz)[num];
	}

	public static <T> Class<T> getTemplateType1(Class<?> clazz) {
		return getTemplateType(clazz, 0);
	}

	public static <T> Class<T> getTemplateType2(Class<?> clazz) {
		return getTemplateType(clazz, 1);
	}

	public static <T> Class<T> getTemplateType3(Class<?> clazz) {
		return getTemplateType(clazz, 2);
	}

	public static <T> Class<T> getTemplateType4(Class<?> clazz) {
		return getTemplateType(clazz, 3);
	}

	public static <T> Class<T> getTemplateType5(Class<?> clazz) {
		return getTemplateType(clazz, 4);
	}

	public static <T> Class<T> getTemplateType6(Class<?> clazz) {
		return getTemplateType(clazz, 5);
	}

	public static <T> Class<T> getTemplateType7(Class<?> clazz) {
		return getTemplateType(clazz, 6);
	}

	public static <T> Class<T> getTemplateType8(Class<?> clazz) {
		return getTemplateType(clazz, 7);
	}

	public static Class<?>[] getSuperTemplateTypes(Class<?> clazz) {
		Type type = clazz.getGenericSuperclass();
		while (true) {
			if (type instanceof ParameterizedType) {
				return getTemplateTypes((ParameterizedType) type);
			}
			if (clazz.getGenericSuperclass() == null) {
				throw new IllegalArgumentException();
			}
			type = clazz.getGenericSuperclass();
			clazz = clazz.getSuperclass();
		}
	}

	public static Class<?>[] getSuperTemplateTypes(Class<?> clazz,
			Class<?> searchedSuperClass) {
		if (clazz == null || searchedSuperClass == null) {
			throw new NullPointerException();
		}
		Class<?> superClass = null;
		do {
			superClass = clazz.getSuperclass();
			if (superClass == searchedSuperClass) {
				break;
			}
		} while ((clazz = superClass) != null);
		if (clazz == null) {
			throw new IllegalArgumentException(
					"The searched for superclass is not a superclass of the given class.");
		}
		final Type type = clazz.getGenericSuperclass();
		if (type instanceof ParameterizedType) {
			return getTemplateTypes((ParameterizedType) type);
		} else {
			throw new IllegalArgumentException(
					"The searched for superclass is not a generic class.");
		}
	}

	public static Class<?>[] getTemplateTypes(ParameterizedType paramterizedType) {
		Class<?>[] types = new Class<?>[paramterizedType
				.getActualTypeArguments().length];
		int i = 0;
		for (Type templateArgument : paramterizedType.getActualTypeArguments()) {
			assert (templateArgument instanceof Class<?>);
			types[i++] = (Class<?>) templateArgument;
		}
		return types;
	}

	public static Class<?>[] getTemplateTypes(Class<?> clazz) {
		Type type = clazz.getGenericSuperclass();
		assert (type instanceof ParameterizedType);
		ParameterizedType paramterizedType = (ParameterizedType) type;
		Class<?>[] types = new Class<?>[paramterizedType
				.getActualTypeArguments().length];
		int i = 0;
		for (Type templateArgument : paramterizedType.getActualTypeArguments()) {
			assert (templateArgument instanceof Class<?>);
			types[i++] = (Class<?>) templateArgument;
		}
		return types;
	}


	
}
